
package urbanparks;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;

import exceptions.FileFormatException;

/**
 * Class that contains all of the data of the list of jobs and the associated behavior.
 * 
 * @author Michelle Miller
 * @version 2-27-12
 * 
 */
public class JobSet {

  /**
   * The number of expected parameters to be read in from the file.
   */
  private static final int EXP_PARAM = 14;

  /**
   * The set of Jobs stored by the system.
   */
  private final Set<Job> my_jobs = new HashSet<Job>();

  /**
   * The number of max jobs pending, read from the configurations file.
   */
  private final int my_max_pending_jobs;

  /**
   * The number of max jobs allowed in a week, read from the configurations file.
   */
  private final int my_max_weekly_jobs;

  /**
   * A reference to the persistent file containing the job data.
   */
  private final File my_file;

  /**
   * The current number of pending jobs.
   */
  private int my_pending_job_count = 0;

  /**
   * The constructor for the JobSet class.
   * 
   * @param the_job_file The file to load the job data from.
   * @param the_max_pending The max number of pending jobs allowed.
   * @param the_max_weekly The max number of jobs allowed in a week.
   */
  public JobSet(final File the_job_file,
      final int the_max_pending, final int the_max_weekly) {
    my_file = the_job_file;
    my_max_pending_jobs = the_max_pending;
    my_max_weekly_jobs = the_max_weekly;
    readFile(the_job_file);
  }

  /**
   * Reads jobs in from the provided persistent file and adds them to the jobset.
   * 
   * @param the_file The persistent job data file.
   */
  private void readFile(final File the_file) {
    String[] tokens;
    Scanner s = null;
    try {
      s = new Scanner(the_file);
    } catch (final FileNotFoundException e) {
      throw new FileFormatException("job file");
    }
    String line = "";
    while (s.hasNextLine()) {
      line = s.nextLine();
      if (line.length() > 0) {
        tokens = line.split(",");
        try {
          addJob(tokens);
        } catch (FileFormatException e) {
          continue;
        }
      }
    }

  }

  /**
   * Parses the line of elements into the proper data types to add the job to the jobset.
   * If there is an error then a FileFormatException is thrown.
   * 
   * @param the_tokens The array of strings read in from the file.
   */
  private void addJob(final String[] the_tokens) throws FileFormatException {
    final String rec_by;
    final String job_desc;
    final String job_loc;
    final String manager_email;
    final List<String> desc; // size == 3
    final List<Integer> max_vol; // size == 3
    final int[] job_dur = new int[4]; // size == 4
    final List<List<String>> volunteers =
        new ArrayList<List<String>>();
    volunteers.add(new ArrayList<String>());
    volunteers.add(new ArrayList<String>());
    volunteers.add(new ArrayList<String>());
    int c = 0;
    for (String s : the_tokens) {
      s.trim();
    }
    if (the_tokens.length >= EXP_PARAM) {
      rec_by = the_tokens[c++];
      job_desc = the_tokens[c++];
      job_loc = the_tokens[c++];
      manager_email = the_tokens[c++];
      desc = new ArrayList<String>();
      desc.add(the_tokens[c++]);
      desc.add(the_tokens[c++]);
      desc.add(the_tokens[c++]);
      max_vol = new ArrayList<Integer>();
      try {
        max_vol.add(Integer.parseInt(the_tokens[c++]));
        max_vol.add(Integer.parseInt(the_tokens[c++]));
        max_vol.add(Integer.parseInt(the_tokens[c++]));
        job_dur[0] = Integer.parseInt(the_tokens[c++]);
        job_dur[1] = Integer.parseInt(the_tokens[c++]);
        job_dur[2] = Integer.parseInt(the_tokens[c++]);
        job_dur[3] = Integer.parseInt(the_tokens[c++]);
      } catch (NumberFormatException e) {
        throw new FileFormatException("job file");
      }
      for (int i = c; i + 1 < the_tokens.length; i += 2) {
        String s = the_tokens[i].toLowerCase();
        if (s.equalsIgnoreCase("low")) {
          volunteers.get(0).add(the_tokens[i + 1]);
        } else if (s.equalsIgnoreCase("medium")) {
          volunteers.get(1).add(the_tokens[i + 1]);
        } else if (s.equalsIgnoreCase("high")) {
          volunteers.get(2).add(the_tokens[i + 1]);
        }
      }
      final Job job =
          new Job(rec_by, job_desc, job_loc, manager_email,
              desc, max_vol, job_dur, volunteers);
      my_jobs.add(job);
      if (job.isPendingJob()) {
        my_pending_job_count++;
      }
    }

  }

  /**
   * Public method to add a new job to the job set.
   * 
   * @param the_rec_by The received by email.
   * @param the_job_desc The job description.
   * @param the_park The park name.
   * @param the_manager_email The park manager's email.
   * @param the_light_desc The light job grade description.
   * @param the_med_desc The medium job grade description.
   * @param the_heavy_desc The heavy job grade description.
   * @param the_light_max The max number of volunteers needed at the light grade.
   * @param the_med_max The max number of volunteers needed at the medium grade.
   * @param the_heavy_max The max number of volunteers needed at the heavy grade.
   * @param the_job_dur The job duration in days.
   * @param the_job_date The start date of the job.
   * @return True if the job was added, or false if there was an error adding the job.
   */
  public boolean addJob(final String the_rec_by,
      final String the_job_desc,
      final String the_park,
      final String the_manager_email,
      final String the_light_desc,
      final String the_med_desc,
      final String the_heavy_desc,
      final int the_light_max,
      final int the_med_max,
      final int the_heavy_max,
      final int the_job_dur,
      final Calendar the_job_date) {
    boolean result = false;
    final List<String> desc = new ArrayList<String>();
    desc.add(the_light_desc);
    desc.add(the_med_desc);
    desc.add(the_heavy_desc);
    final List<Integer> max = new ArrayList<Integer>();
    max.add(the_light_max);
    max.add(the_med_max);
    max.add(the_heavy_max);
    final int[] job_dur = {the_job_date.get(Calendar.YEAR),
        the_job_date.get(Calendar.MONTH),
        the_job_date.get(Calendar.DAY_OF_MONTH),
        the_job_dur};
    final Job job =
        new Job(the_rec_by, the_job_desc, the_park, the_manager_email,
            desc, max, job_dur);
    if (job != null && !isConflict(job)) {
      my_jobs.add(job);
      my_pending_job_count++;
      result = true;
    }
    return result;
  }

  /**
   * Method that determines if a user is already signed up for a job on the date provided.
   * 
   * @param the_user The user to check.
   * @param the_date The date to check the jobs on.
   * @return True if the user is signed up for a job on the date, false if not.
   */
  public boolean isConflict(final User the_user,
      final Date the_date) {
    boolean result = false;
    final Calendar date = Calendar.getInstance();
    date.setTime(the_date);
    List<Job> jobs = getJobsInDay(date);
    for (Job j : jobs) {
      if (j.containsVolunteer(the_user.getEmailAddress())) {
        result = true;
      }
    }
    return result;
  }
  
  /**
   * Returns a list of the jobs occurring on the date provided.
   * 
   * @param the_date The date to get the jobs on.
   * @return A list of the jobs occurring on the provided date.
   */
  private List<Job> getJobsInDay(final Calendar the_date) {
    final List<Job> result = new ArrayList<Job>();
    final int day = the_date.get(Calendar.DAY_OF_YEAR);
    final int year = the_date.get(Calendar.YEAR);
    for (Job j : my_jobs) {
      final int jday = j.getJobDuration().getDayOfYear();
      final int jyear = j.getJobDuration().getYear();
      if (jyear == year && jday == day) { // if the_date is near year
        result.add(j);
      }
    }
    return result;
  }

  /**
   * Method that determines if a job is conflicting with any business rules. This method
   * checks against the max pending count, the max weekly jobs count, ensures that the job
   * is pending and not out of the 3 month range.
   * 
   * @param the_job The job to check.
   * @return True if the job cannot be added, false if not.
   */
  public boolean isConflict(final Job the_job) {
    boolean result = false;
    final Calendar date = Calendar.getInstance();
    date.setTime(the_job.getJobDuration().getStartTime());
    date.get(Calendar.MONTH);
    result =
        my_pending_job_count >= my_max_pending_jobs || 
        getJobsInWeek(date).size() >= my_max_weekly_jobs ||
        !the_job.isPendingJob() ||
        isJobOutOfRange(the_job);
        return result;
  }

  /**
   * Determines if the job is more than 3 months in the future from the current date.
   * 
   * @param the_job The job to check against.
   * @return True if the job is out of range, false if not.
   */
  private boolean isJobOutOfRange(final Job the_job) {
    boolean result = true;
    final Calendar max = Calendar.getInstance();
    max.add(Calendar.MONTH, 3);
    if (max.get(Calendar.YEAR) > the_job.getJobDuration().getYear()) {
      result = false;
    } else if (max.get(Calendar.YEAR) == the_job.getJobDuration().getYear()) {
      result = max.get(Calendar.DAY_OF_YEAR) < the_job.getJobDuration().getDayOfYear();
      //if the current day of year is greater than the max day of year then the job is out of range
    }    
    return result;
  }

  /**
   * Returns a list of the jobs occurring on three days on either side of the provided date.
   * 
   * @param the_date The date to get the jobs near.
   * @return A list of the jobs occurring on three days on either side of the provided day.
   */
  private List<Job> getJobsInWeek(final Calendar the_date) {
    final List<Job> result = new ArrayList<Job>();
    final Calendar start_date = the_date;
    final Calendar end_date = the_date;
    
    start_date.add(Calendar.DAY_OF_YEAR, -3);
    final int sdoy = start_date.get(Calendar.DAY_OF_YEAR);
    final int sy = start_date.get(Calendar.YEAR);
    
    end_date.add(Calendar.DAY_OF_YEAR, 6);
    final int edoy = end_date.get(Calendar.DAY_OF_YEAR);
    final int ey = end_date.get(Calendar.YEAR);
    
    end_date.add(Calendar.DAY_OF_YEAR, 3);
    for (Job j : my_jobs) {
      
      final int doy = j.getJobDuration().getDayOfYear();
      final int y = j.getJobDuration().getYear();
      
      if (sy == ey) {
        if (doy > sdoy && doy < edoy) {
          result.add(j);
        }
      } else { //ey > sy
        if (doy > sdoy && y == sy ||
            doy < edoy && y == ey) {
          result.add(j);
        }
      }
    }
    return result;
  }

  /**
   * Returns a list of the jobs that are occurring in the provided year and month.
   * 
   * @param the_month The month to check.
   * @param the_year The Year to check.
   * @return The list of jobs occurring in the provided month and the provided year.
   */
  public List<Job> getJobsByMonthYear(final int the_month,
      final int the_year) {
    List<Job> result = new ArrayList<Job>();
    
    for (Job j : my_jobs) {
      final JobDuration jd = j.getJobDuration();
      final int month = jd.getMonth();
      final int year = jd.getYear();
      if (month == the_month && year == the_year) {
        result.add(j);
      }
    }
    return result;
  }

  /**
   * Returns a list of the jobs where the job start date is greater than or equal to the
   * current day.
   * 
   * @return A list of the jobs that start on or after the current day.
   */
  public List<Job> getUpcomingJobs() {
    List<Job> result = new ArrayList<Job>();
    for (Job j : my_jobs) {
      if (j.isPendingJob()) {
        result.add(j);
      }
    }
    return result;
  }

  /**
   * Returns a list of the jobs occurring in parks managed by the provided user email.
   * 
   * @param the_manager_email The email of the park manager to search for.
   * @return A list of the jobs occurring in parks managed by the provided park manager.
   */
  public List<Job> getJobsByParkManager(final String the_manager_email) {
    List<Job> result = new ArrayList<Job>();
    for (Job j : my_jobs) {
      if (j.getParkManager().equalsIgnoreCase(the_manager_email)) {
        result.add(j);
      }
    }
    return result;
  
  }

  /**
   * Returns a list of the jobs that the provided user is volunteered for.
   * 
   * @param the_vol_email The email of the volunteer to search for.
   * @return A list of the jobs that the provided user is volunteered for.
   */
  public List<Job> getJobsByVolunteer(final String the_vol_email) {
    List<Job> result = new ArrayList<Job>();
    for (Job j : my_jobs) {
      if (j.containsVolunteer(the_vol_email)) {
        result.add(j);
      }
    }
    return result;
  }

  /**
   * Returns a list of the jobs occurring in the provided park.
   * 
   * @param the_park_name The name of the park to search for.
   * @return A list of the jobs occurring in the provided park.
   */
  public List<Job> getUpcomingJobsByPark(final String the_park_name) {
    List<Job> result = new ArrayList<Job>();
    for (Job j : getUpcomingJobs()) {
      if (j.getPark().equalsIgnoreCase(the_park_name)) {
        result.add(j);
      }
    }
    return result;
  }

  /**
   * Returns a list of the jobs occurring in the provided city.
   * 
   * @param the_city The city to search for.
   * @param the_park_set A reference to the ParkSet.
   * @return A list of the jobs occurring in the provided city.
   */
  public List<Job> getUpcomingJobsByRegion(final String the_city,
      final ParkSet the_park_set) {
    List<Job> result = new ArrayList<Job>();
    for (Job j : getUpcomingJobs()) {
      if (the_park_set.getPark(j.getPark()).getCity()
          .equalsIgnoreCase(the_city)) {
        result.add(j);
      }
    }
    return result;
  }

  /**
   * Returns a list of the jobs occurring in the provided park district.
   * 
   * @param the_district The park district to search for.
   * @param the_park_set A reference to the ParkSet.
   * @return A list of the jobs occurring in the provided park district.
   */
  public List<Job> getUpcomingJobsByDistrict(final String the_district,
      final ParkSet the_park_set) {
    List<Job> result = new ArrayList<Job>();
    for (Job j : getUpcomingJobs()) {
      final Park p = the_park_set.getPark(j.getPark());
      if (p.getParkDistrict().equalsIgnoreCase(the_district)) {
        result.add(j);
      }
    }
    return result;
  }

  /**
   * The method that writes the current JobSet to the persistent data file.
   */
  public void writeFile() {
    try {
      FileOutputStream out =
          new FileOutputStream(my_file, false);
      
      for (Job j : my_jobs) {
        out.write(j.getFileString().getBytes());
        out.write("\n".getBytes());
      }

      out.close();
    } catch (IOException e) {
    }
  }

}
